// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include <crypto/aead.h>
#include <crypto/bytes.h>
#include <fbl/algorithm.h>
#include <fbl/auto_call.h>
#include <fbl/unique_ptr.h>
#include <lib/zircon-internal/debug.h>
#include <zircon/errors.h>
#include <zircon/types.h>

// See note in //zircon/third_party/ulib/uboringssl/rules.mk
#define BORINGSSL_NO_CXX
#include <openssl/aead.h>

#include "error.h"

#define ZXDEBUG 0

namespace crypto {

// The previously opaque crypto implementation context.  Guaranteed to clean up on destruction.
struct AEAD::Context {
  Context() {}

  ~Context() { EVP_AEAD_CTX_cleanup(&impl); }

  EVP_AEAD_CTX impl;
};

namespace {

// Get the aead for the given |version|.
zx_status_t GetAEAD(AEAD::Algorithm aead, const EVP_AEAD** out) {
  switch (aead) {
    case AEAD::kUninitialized:
      xprintf("not initialized\n");
      return ZX_ERR_INVALID_ARGS;

    case AEAD::kAES128_GCM:
      *out = EVP_aead_aes_128_gcm();
      return ZX_OK;

    case AEAD::kAES128_GCM_SIV:
      *out = EVP_aead_aes_128_gcm_siv();
      return ZX_OK;

    default:
      xprintf("invalid aead = %u\n", aead);
      return ZX_ERR_NOT_SUPPORTED;
  }
}

}  // namespace

// Public methods

zx_status_t AEAD::GetKeyLen(Algorithm algo, size_t* out) {
  zx_status_t rc;

  if (!out) {
    xprintf("missing output pointer\n");
    return ZX_ERR_INVALID_ARGS;
  }
  const EVP_AEAD* aead;
  if ((rc = GetAEAD(algo, &aead)) != ZX_OK) {
    return rc;
  }
  *out = EVP_AEAD_key_length(aead);

  return ZX_OK;
}

zx_status_t AEAD::GetIVLen(Algorithm algo, size_t* out) {
  zx_status_t rc;

  if (!out) {
    xprintf("missing output pointer\n");
    return ZX_ERR_INVALID_ARGS;
  }
  const EVP_AEAD* aead;
  if ((rc = GetAEAD(algo, &aead)) != ZX_OK) {
    return rc;
  }
  *out = EVP_AEAD_nonce_length(aead);

  return ZX_OK;
}

zx_status_t AEAD::GetTagLen(Algorithm algo, size_t* out) {
  zx_status_t rc;

  if (!out) {
    xprintf("missing output pointer\n");
    return ZX_ERR_INVALID_ARGS;
  }
  const EVP_AEAD* aead;
  if ((rc = GetAEAD(algo, &aead)) != ZX_OK) {
    return rc;
  }
  *out = EVP_AEAD_max_tag_len(aead);

  return ZX_OK;
}

AEAD::AEAD() : ctx_(nullptr), direction_(Cipher::kUnset), tag_len_(0) {}

AEAD::~AEAD() {}

zx_status_t AEAD::Seal(const Bytes& ptext, const uint8_t* aad, size_t aad_len, uint64_t* out_nonce,
                       Bytes* out_ctext) {
  zx_status_t rc;

  if (direction_ != Cipher::kEncrypt) {
    xprintf("not configured to encrypt\n");
    return ZX_ERR_BAD_STATE;
  }

  size_t ptext_len = ptext.len();
  if (!out_nonce || !out_ctext) {
    xprintf("bad parameter(s): out_nonce=%p, ctext=%p\n", out_nonce, out_ctext);
    return ZX_ERR_INVALID_ARGS;
  }

  // If the caller recycles the |Bytes| used for|ctext|, this becomes a no-op.
  size_t ctext_len = ptext_len + tag_len_;
  if ((rc = out_ctext->Resize(ctext_len)) != ZX_OK) {
    return rc;
  }

  uint8_t* iv8 = reinterpret_cast<uint8_t*>(iv_.get());
  size_t out_len;
  if (EVP_AEAD_CTX_seal(&ctx_->impl, out_ctext->get(), &out_len, ctext_len, iv8, iv_len_,
                        ptext.get(), ptext_len, aad, aad_len) != 1) {
    xprintf_crypto_errors(&rc);
    return rc;
  }
  if (out_len != ctext_len) {
    xprintf("length mismatch: expected %zu, got %zu\n", ptext_len, out_len);
    return ZX_ERR_INTERNAL;
  }

  // Increment nonce
  uint64_t nonce = iv_[0];
  iv_[0] += 1;
  if (iv_[0] == iv0_) {
    xprintf("exceeded maximum operations with this key\n");
    return ZX_ERR_BAD_STATE;
  }

  *out_nonce = nonce;
  return ZX_OK;
}

zx_status_t AEAD::Open(uint64_t nonce, const Bytes& ctext, const uint8_t* aad, size_t aad_len,
                       Bytes* out_ptext) {
  zx_status_t rc;

  if (direction_ != Cipher::kDecrypt) {
    xprintf("not configured to decrypt\n");
    return ZX_ERR_BAD_STATE;
  }

  size_t ctext_len = ctext.len();
  if (ctext_len < tag_len_ || !out_ptext) {
    xprintf("bad parameter(s): ctext.len=%zu, ptext=%p\n", ctext_len, out_ptext);
    return ZX_ERR_INVALID_ARGS;
  }

  size_t ptext_len = ctext_len - tag_len_;
  if ((rc = out_ptext->Resize(ptext_len)) != ZX_OK) {
    return rc;
  }

  // Inject nonce
  iv_[0] = nonce;
  uint8_t* iv8 = reinterpret_cast<uint8_t*>(iv_.get());
  size_t out_len;
  if (EVP_AEAD_CTX_open(&ctx_->impl, out_ptext->get(), &out_len, ptext_len, iv8, iv_len_,
                        ctext.get(), ctext_len, aad, aad_len) != 1) {
    xprintf_crypto_errors(&rc);
    return rc;
  }
  if (out_len != ptext_len) {
    xprintf("length mismatch: expected %zu, got %zu\n", ptext_len, out_len);
    return ZX_ERR_INTERNAL;
  }

  return ZX_OK;
}

void AEAD::Reset() {
  ctx_.reset();
  direction_ = Cipher::kUnset;
  iv_len_ = 0;
  tag_len_ = 0;
}

// Private methods

zx_status_t AEAD::Init(Algorithm algo, const Secret& key, const Bytes& iv,
                       Cipher::Direction direction) {
  zx_status_t rc;

  Reset();
  auto cleanup = fbl::MakeAutoCall([&] { Reset(); });

  // Look up specific algorithm
  const EVP_AEAD* aead;
  if ((rc = GetAEAD(algo, &aead)) != ZX_OK) {
    return rc;
  }
  size_t key_len = EVP_AEAD_key_length(aead);
  iv_len_ = EVP_AEAD_nonce_length(aead);
  tag_len_ = EVP_AEAD_max_tag_len(aead);

  // Check parameters
  if (key.len() != key_len) {
    xprintf("wrong key length; have %zu, need %zu\n", key.len(), key_len);
    return ZX_ERR_INVALID_ARGS;
  }
  if (iv.len() != iv_len_) {
    xprintf("wrong IV length; have %zu, need %zu\n", iv.len(), iv_len_);
    return ZX_ERR_INVALID_ARGS;
  }

  // Allocate context
  fbl::AllocChecker ac;
  ctx_.reset(new (&ac) Context());
  if (!ac.check()) {
    xprintf("allocation failed: %zu bytes\n", sizeof(Context));
    return ZX_ERR_NO_MEMORY;
  }

  // Initialize context
  if (EVP_AEAD_CTX_init(&ctx_->impl, aead, key.get(), key.len(), EVP_AEAD_DEFAULT_TAG_LENGTH,
                        nullptr) != 1) {
    xprintf_crypto_errors(&rc);
    return rc;
  }
  direction_ = direction;

  // Reserve space for IV
  size_t n = fbl::round_up(iv_len_, sizeof(uint64_t)) / sizeof(uint64_t);
  iv_.reset(new (&ac) uint64_t[n]{0});
  if (!ac.check()) {
    xprintf("failed to allocate %zu bytes\n", n * sizeof(uint64_t));
    return ZX_ERR_NO_MEMORY;
  }
  memcpy(iv_.get(), iv.get(), iv_len_);

  cleanup.cancel();
  return ZX_OK;
}

}  // namespace crypto
